3
以下这段都是废话,请跳过

公司移动端开发平台进行了大变革,前端架构由DCloud大生态转换为VUE,所以移动端的UI组件库从MUI改为使用MintUI,然后开始大刀阔斧的把MintUI组件改成MUI组件的样子,然后发现少了几个较为常用的,其中一个就是,嗯,侧滑面板(也叫侧滑菜单,也叫抽屉面板-andriod官方是这么翻译的,很形象)。但是,它就是一个布局组件,具体里边菜单什么的,那都是浮云(嗯,就是爱用几年前的流行词汇,而且很喜欢在网上冲浪和踩别人的空间)

以上这段都是废话,感谢阅读

需求

  1. 开发一个侧滑面板(类似QQ、网易邮箱等app的)
  2. 可以在左边,也可以在右边
  3. 侧滑面板内容随意定制
  4. 侧滑面板相对的就有主面板,那么衍生出不同的体位和姿势

    (1)主面板滑动,侧滑面板不动
    (2)侧滑面板动,主面板不动
    (3)它俩一块动,一起--------

  5. 代码风格尽量和MintUI的其他组件风格类似(这个挺重要的)

参考

mintUI组件中同样?️滑动操作的tabContainer,为了满足需求5,我连函数名都抄了过来。

不说废话,上代码吧

The Waaaaaaaay

1. 设计组件结构

这个组件分为两部分,一部分为侧滑面板容器,另一部分为主面板容器,然后具体容器内部直接放了插槽,然后还需要一个主面板容器的遮罩,为了侧滑面板打开的时候显现出来。上代码了

<div class="mint-drawer-layout">

    <!--侧滑栏-->
    <div
      ref="drawer"
      class=" mint-drawer-warp"
      @touchstart.stop="startDrag"
      @touchmove.stop="onDrag"
      @touchend.stop="endDrag"
      :style="drawerStyle">
      <slot name="drawer"></slot>
    </div>

    <!--主容器-->
    <div
      ref="content"
      @touchstart.stop="startDrag"
      @touchmove.stop="onDrag"
      @touchend.stop="endDrag"
      class=" mint-content-warp"
      :style="contentStyle">
      <!--主容器遮罩(侧滑打开状态下显示)-->
      <div class="content-mask" v-tap="toggle" ref="contentMask"></div>
      <slot name="content"></slot>
    </div>
  </div>

2. 配置设计

这块加了一些我们公司的一些需求,可能各位哥哥姐姐门用不到里边的一些props的设计,仅供参考

props: {
      // 侧滑面板的宽度(单位px)
      'drawerWidth': {
        type: Number,
        default: 200
      },
      // 是否可用
      'enable': {
        type: Boolean,
        default: true
      },
      // 侧滑菜单是否在右边,默认为false,在左边
      'isRight': {
        type: Boolean,
        default: false
      },
      // 侧滑菜单滑动操作类型
      // ['fixDrawer'——固定侧滑面板,主面板滑动]
      // ['fixContent'——固定主面板,侧滑面板滑动]
      // ['noFixed'——一起滑动!]
      'swipeType': {
        type: String,
        default: 'fixDrawer'
      },
      // 点击出现侧滑菜单的按钮的id ( @TODO 这里如何处理异步渲染的问题 )
      'btnId': {
        type: String,
        default: ''
      },
      // 状态位,侧滑面板是否为打开状态
      // (因为我们公司有这种一开始就把侧滑菜单打开的shabee场景,所以这才会有这么个东西)
      //(如果这个不希望配置的话、可以放在data里边)
      'isDrawerOpened': {
        type: Boolean,
        default: false
      },
      // 是否可滑动,如果不可滑动的话,就只能通过调用toogle方法打开侧滑面板
      // 这个也是公司的一个使用场景,就是你甭滑,找个按钮触发一下侧滑面板打开的方法才能打开
      //(如果这个不希望配置的话、也可以放在data里边)
      'swipeable': {
        type: Boolean,
        default: true
      }
    }

2. 样式设计

不得不承认,我的css写的shit

对于整个的组件来说,它应该是默认充满整个父容器的,而且这个组件,我觉得,一般都是用来放在最外层的一个布局组件,所以,默认充满窗口就行了
所以组件的最外层来一个绝对布局,然后如下:

.mint-drawer-layout {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    overflow-x: hidden;
}

然后侧滑面板,只需要纵向充满就可以了,宽度是配置

.mint-drawer-warp {
    position: absolute;
    top: 0;
    bottom: 0;
}

然后是主面板,没说的

.mint-content-warp {
    position: relative;
    width: 100%;
    height: 100%;
}

4. 关键实现

终于来到了介绍到底该怎么实现的了

滑动处理三步走,start、drag、end

首先第一步上来就要设置一个记录滑动操作的状态变量——dragging,设置为false,方便屏蔽滑动时触发的其他操作的执行

——开始的时候记录开始滑动的位置
——滑动中,dragging状态记录为true,开始进行滑动位置和手指移动的联动
——滑动结束,dragging状态记录为false,计算当前的滑动位置,判断是划开侧滑面板还是关闭,并进行动画处理

其中几个细节小谈一哈
(1)左右滑动操作触发的判断:我这边是公司的规范,横轴移动位移大于五,竖轴位移不大于横轴的1.73倍就可以
(2)最后结束时判断侧滑面板的打开和关闭:是这样的,我这边取的是三分之一的侧滑面板的宽度,也就是从打开到关闭,那么像关闭的方向滑动侧滑面板宽度的三分之一就可以了,如果是关闭到打开,往打开的方向滑动三分之一就可以了
(3)左侧和右侧,还有三种不同的滑动方式:三种不同的滑动方式实际上就是控制到底哪个面板随着手指动,具体的动作过程和面板的偏移量实际上是一样的。左右两侧就更简单了,直接是对称的操作就可

滑动结束的操作,参考的tabcontainer,也挺巧妙的,各位请上眼~

      /**
       * 滑动结束的动画
       */
      swipeLeaveTransition() {
        let g = this, currentMovingDoms = [];
        let {swipeType, drawerWidth} = g;

        switch (swipeType) {
          case 'fixDrawer':
            currentMovingDoms.push(g.content);
            break;
          case 'fixContent':
            currentMovingDoms.push(g.drawer);
            break;
          case 'noFixed':
            currentMovingDoms.push(g.drawer);
            currentMovingDoms.push(g.content);
            break;
          default:
            break;
        }

        currentMovingDoms.forEach((val) => {
          val.classList.add('swipe-transition');
        });

        setTimeout(() => {
          if (g.isDO) {
            this.swipeMove(drawerWidth);
          } else {
            this.swipeMove(0);
            g.contentMask.style.opacity = 0;
            g.contentMask.style.display = 'none';
          }

          g.isToggle = false;

          currentMovingDoms.forEach((val) => {
            once(val, 'webkitTransitionEnd', _ => {
              val.classList.remove('swipe-transition');
              g.swiping = false;
            });
          });


        }, 0);
      },

      /**
       * 滑动操作
       * @param offset 滑动位置
       */
      swipeMove(offset) {
        let g = this;
        let {swipeType, isRight} = g;

        g.contentMask.style.display = 'block';
        g.contentMask.style.opacity = Math.abs(offset) / g.drawerWidth * 0.4;

        switch (swipeType) {
          case 'fixDrawer':
            g.content.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
            g.swiping = true;
            break;
          case 'fixContent':
            g.drawer.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
            g.swiping = true;
            break;
          case 'noFixed':
            g.content.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
            g.drawer.style.webkitTransform = `translate3d(${(!isRight ? '' : '-') + offset}px, 0, 0)`;
            g.swiping = true;
            break;
          default:
            break;
        }
      },
      
      // 开始滑动
      startDrag(evt) {
        let g = this;
        if (!g.enable || !g.swipeable) return false;

        evt = evt.changedTouches ? evt.changedTouches[0] : evt;

        g.start.x = evt.pageX;
        g.start.y = evt.pageY;
      },

      // 滑动中
      onDrag(evt) {
        let g = this, swiping;
        if (!g.enable || !g.swipeable) return false;

        g.dragging = true;

        const e = evt.changedTouches ? evt.changedTouches[0] : evt;
        const offsetTop = e.pageY - g.start.y;
        const offsetLeft = e.pageX - g.start.x;
        const y = Math.abs(offsetTop);
        const x = Math.abs(offsetLeft);

        swiping = !(x < 5 || (x >= 5 && y >= x * 1.73));
        if (!swiping) return;
        evt.preventDefault();

        let offset;

        if (g.isDO) {
          offset = g.isRight ? (g.drawerWidth - offsetLeft) : (g.drawerWidth - (-offsetLeft));
        } else {
          offset = g.isRight ? -offsetLeft : offsetLeft;
        }

        if (offset < 0 || offset > g.drawerWidth) {
          g.swiping = false;
          return;
        }
        g.offset = offset;
        g.swipeMove(offset);
      },

      // 结束滑动
      endDrag() {
        let g = this;

        if (!g.enable || g.isToggle || !g.dragging) {
          return false;
        }

        const tempWidth = g.drawerWidth / 3;

        if (g.isDO && g.offset < tempWidth * 2) {
          g.isDO = false;
        } else if (!g.isDO && g.offset > tempWidth) {
          g.isDO = true;
        }
        g.dragging = false;

        g.swipeLeaveTransition();
      }

好啦,到这应该就差不多了。。。
里边涉及到的v-tap指令是自定义的指令,为了处理移动端的点击操作,我还整理了一片陋文:https://segmentfault.com/a/11... (移动点击长按滑动vue指令)

然后这个组件的源码我放在了我fork出来的mintUI项目上
https://github.com/LylaYuKako...

谢谢各位品尝,


yurt
105 声望4 粉丝

1233